﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Data.Objects;
using System.Data.Objects.DataClasses;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using RiaServicesContrib;

namespace EntityGraph.SQL
{
    public class EntityGraphShape4SQL : EntityGraphShape
    {

        private ObjectContext ObjectContext { get; set; }
        public EntityGraphShape4SQL(ObjectContext ctx)
        {
            this.ObjectContext = ctx;
        }
        EntityGraphPathCollection _entityGraphPathCollection;
        internal EntityGraphPathCollection EntityGraphPathCollection
        {
            get
            {
                if(_entityGraphPathCollection == null)
                {
                    _entityGraphPathCollection = new EntityGraphPathCollection(this);
                    foreach(var node in _entityGraphPathCollection.SelectMany(path => path))
                    {
                        node.TypeTree = TypeTree.Create(ObjectContext, node.Type);
                    }
                }
                return _entityGraphPathCollection;
            }
        }
        private void LoadPaths<TEntity>(IEnumerable<EntityGraphPath> paths)
        {
            List<SqlSelect> queries = new List<SqlSelect>();
            List<TypeTree> typeTrees = new List<TypeTree>();
            var typeTree = TypeTree.Create(ObjectContext, typeof(TEntity)).Root;
            SqlSelect sql = typeTree.ToSql();
            queries.Add(sql);
            typeTrees.Add(typeTree);
            foreach(var path in paths)
            {
                sql = path.ToSql(ObjectContext);
                queries.Add(sql);
                typeTrees.Add(path.Last().TypeTree.Root);
            }
            var queryPlan = new QueryPlan(queries, paths, typeTrees);
            cache.Add(typeof(TEntity), queryPlan);
            SqlSelect.ExecuteAndAttachToContext(ObjectContext, queryPlan);
        }
        private void LoadPaths(object entity, IEnumerable<EntityGraphPath> paths)
        {
            Debug.Assert(entity != null);
            List<SqlSelect> queries = new List<SqlSelect>();
            List<TypeTree> typeTrees = new List<TypeTree>();
            foreach (var path in paths)
            {
                SqlSelectFromTable sql;
                sql = path.ToSql(ObjectContext, entity) as SqlSelectFromTable;

                queries.Add(new SqlSelectFromTableWithParameterBinding(sql.FromTable, sql.Table, sql.Where));
                typeTrees.Add(path.Last().TypeTree.Root);
            }
            var queryPlan = new QueryPlan(queries, paths, typeTrees);
            queryPlan.Bind(entity);
            cache.Add(entity.GetType(), queryPlan);
            SqlSelect.ExecuteAndAttachToContext(ObjectContext, queryPlan);
        }

        public override object GetNode(object entity, PropertyInfo edge)
        {
            var paths = this.EntityGraphPathCollection.Where(p => p[0].OutEdge.EdgeInfo == edge && p.Isloaded == false).ToList();
            if(paths.Any())
            {
                LoadPaths(entity, paths);
            }
            return edge.GetValue(entity, null);
        }
        public override IEnumerable GetNodes(object entity, PropertyInfo edge)
        {
            var paths = this.EntityGraphPathCollection.Where(p => p[0].OutEdge.EdgeInfo == edge && p.Isloaded == false).ToList();
            if(paths.Any())
            {
                LoadPaths(entity, paths);
            }
            var relatedEnd = (RelatedEnd)edge.GetValue(entity, null);
            return (IEnumerable)relatedEnd;
        }
        private bool IsMatchingPathForType(EntityGraphPath path, Type type)
        {
            foreach(var node in path)
            {
                if(node.Type == type)
                    return true;
                if(node.Type.IsAssignableFrom(type))
                {
                    return true;
                }
            }
            return false;
        }
        static Dictionary<Type, QueryPlan> cache = new Dictionary<Type, QueryPlan>();
        /// <summary>
        /// Loads (by querying a data base) all entities that of this entity graph shape. The query
        /// starts with any edge that have an in-type of TEntity.
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        public void Load<TEntity>()
        {
            var type = typeof(TEntity);
            if(cache.ContainsKey(typeof(TEntity)))
            {
                SqlSelect.ExecuteAndAttachToContext(ObjectContext, cache[type]);
            }
            else
            {
                var paths = this.EntityGraphPathCollection.Where(p => IsMatchingPathForType(p, type) && p.Isloaded == false).ToList();
                LoadPaths<TEntity>(paths);
            }
        }
        /// <summary>
        /// Loads (by querying a data base) all entities that of this entity graph shape. The query
        /// starts with the provided entity.
        /// </summary>
        /// <param name="entity"></param>
        public void Load(object entity)
        {
            var type = entity.GetType();
            if(cache.ContainsKey(type))
            {
                var entry = cache[type];
                entry.Bind(entity);
                SqlSelect.ExecuteAndAttachToContext(ObjectContext, cache[type]);
            }
            else
            {
                var paths = this.EntityGraphPathCollection.Where(p => IsMatchingPathForType(p, type) && p.Isloaded == false).ToList();
                if(paths.Any())
                {
                    LoadPaths(entity, paths);
                }
            }
        }

        new public EntityGraphShape4SQL Edge<TLHS, TRHS>(Expression<EntityGraphShape.EdgeEnumType<TLHS, TRHS>> edge)
        {
            base.Edge<TLHS, TRHS>(edge);
            return this;
        }
        new public EntityGraphShape4SQL Edge<TLHS, TRHS>(Expression<EntityGraphShape.EdgeType<TLHS, TRHS>> edge)
        {
            base.Edge<TLHS, TRHS>(edge);
            return this;
        }
    }
}
